﻿using System;
using System.Collections.Generic;
using System.Web;
using System.Web.Mvc;
using RESTController.ActionFilters;
using RESTController.Lib;

namespace RESTController
{
    /// <summary>
    /// A resource is an object (a domain entity) that may support the basic CRUD operations (list, show, new, edit, delete).
    /// </summary>
    public abstract class ResourceController<TModel> : Controller where TModel : class
    {
        protected int pageIndex;

        /// <summary>
        /// Gets the number of items to show on a list page.
        /// </summary>
        protected virtual int PageSize
        {
            get { return RESTControllerSettings.Settings.PageSize; }
        }

        /// <summary>
        /// If true, successful new and edit operations will redirect to the index view. If false, successful new and edit operations will redirect to the show view.
        /// </summary>
        protected virtual bool RedirectToIndex
        {
            get { return RESTControllerSettings.Settings.RedirectToIndex; }
        }

        /// <summary>
        /// Should the List action for this controller use paging?
        /// </summary>
        protected virtual bool UsePaging
        {
            get { return RESTControllerSettings.Settings.UsePaging; }
        }
        
        public virtual ActionResult Count()
        {
            OnCountRequested();
            var modelCount = GetCount();
            var count = GetPageCount(modelCount);
            return (RESTControllerSettings.Settings.AllowJsonGet) ? Json(new { count }, JsonRequestBehavior.AllowGet) : Json(new { count });
        }

        [UseDeleteMethodAsConfigured]
        public virtual ActionResult Delete(object id)
        {
            try
            {
                OnDeleteRequested(id);            
                DeleteModel(id);
                OnDeleteSuccessful(id);

                return Request.IsAjaxRequest() ? ReturnDeleteSuccessfulXhr(id) : ReturnDeleteSuccessful(id);
            }
            catch (Exception ex)
            {
                OnDeleteError(ex);
                return (Request.IsAjaxRequest()) ? ReturnDeleteErrorXhr(id, ex) : ReturnDeleteError(id);
            }            
        }

        [HttpGet]
        public virtual ActionResult Edit(object id)
        {
            OnEditRequested(id);
            var model = GetModel(id);

            return (Request.IsAjaxRequest()) ? ReturnEditXhr(model) : ReturnEdit(model);
        }

        [UseEditMethodAsConfigured]
        public virtual ActionResult Edit(object id, TModel model)
        {
            try
            {
                OnEditRequested(id);
                if (ModelState.IsValid)
                {
                    var originalModel = GetModel(id);
                    UpdateResource(originalModel, model);
                    SaveModel(originalModel);
                    OnEditSuccessful(originalModel);

                    return (Request.IsAjaxRequest()) ? ReturnEditSuccessfulXhr(id, model) : ReturnEditSuccessful(id, model);
                }
                OnEditModelStateInvalid(model);                
            }
            catch (Exception exception)
            {
                OnEditError(exception);                                
            }
            return (Request.IsAjaxRequest()) ? ReturnEditXhr(model) : ReturnEdit(model);
        }

        public virtual ActionResult Index()
        {
            OnIndexRequested();
            return List(1);
        }

        public virtual ActionResult List(int? page)
        {
            OnListRequested(page);            
            var models = GetModels();
            IEnumerable<TModel> collection = (UsePaging) ? new PagedList<TModel>(models, pageIndex, RESTControllerSettings.Settings.PageSize) : models;
            return (Request.IsAjaxRequest()) ? ReturnListXhr(collection) : ReturnList(collection);
        }

        [HttpGet]
        public virtual ActionResult New()
        {
            OnNewRequested();
            var model = GetDefaultModel();
            return (Request.IsAjaxRequest()) ? ReturnNewXhr(model) : ReturnNew(model);
        }

        [UseNewMethodAsConfigured]
        public virtual ActionResult New(TModel model)
        {            
            try
            {
                OnNewRequested();
                if (ModelState.IsValid)
                {
                    var id = AddModel(model);
                    OnNewSuccessful(model);

                    return (Request.IsAjaxRequest()) ? ReturnNewSuccessfulXhr(id, model) : ReturnNewSuccessful(id, model);
                }

                // Model state is not valid.
                OnNewModelStateInvalid(model);                
            }
            catch (Exception ex)
            {
                OnNewError(ex);                
            }
            return (Request.IsAjaxRequest()) ? ReturnNewXhr(model) : ReturnNew(model);
        }

        public virtual ActionResult Show(object id)
        {
            OnShowRequested(id);
            var model = GetModel(id);

            return (Request.IsAjaxRequest()) ? ReturnShowXhr(model) : ReturnShow(model);
        }

        #region Data operations

        /// <summary>
        /// Add a model to the data store.
        /// </summary>
        /// <param name="model">Model to add.</param>
        /// <returns>Unique ID of the newly created model.</returns>
        protected abstract object AddModel(TModel model);

        /// <summary>
        /// Delete a model from the data store.
        /// </summary>
        /// <param name="id">ID of model to be deleted</param>
        protected abstract void DeleteModel(object id);

        /// <summary>
        /// GetCount is required to support paging. If paging is not used, this operation is not necessary.
        /// </summary>
        /// <returns>Total number of entities.</returns>
        protected virtual int GetCount()
        {
            return 0;
        }

        /// <summary>
        /// Get the number of pages. Override if using custom page calculation method.
        /// </summary>
        /// <returns>Number of pages.</returns>
        protected virtual int GetPageCount(int modelCount)
        {
            return PageCountCalculator.CalculateNumberOfPages(PageSize, modelCount);
        }

        /// <summary>
        /// Get the default model state. By default, Activator.CreateInstance() will create a new model; however,
        /// this may not be sufficient for some models.
        /// </summary>
        /// <returns>Instance of model.</returns>
        protected virtual TModel GetDefaultModel()
        {
            return Activator.CreateInstance<TModel>();
        }

        /// <summary>
        /// Get a model with the given ID from the data store.
        /// </summary>
        /// <param name="id">ID</param>
        /// <returns>Instance of model</returns>
        protected abstract TModel GetModel(object id);

        /// <summary>
        /// Get a collection of models from the data store.
        /// </summary>
        /// <returns>Collection of models.</returns>
        protected abstract IEnumerable<TModel> GetModels();

        /// <summary>
        /// Save a model in the data store.
        /// </summary>
        /// <param name="model">Model to be saved.</param>
        protected abstract void SaveModel(TModel model);

        #endregion

        #region Event extension methods

        /// <summary>
        /// Notify that count has been requested.
        /// </summary>
        protected virtual void OnCountRequested()
        {                        
        }

        /// <summary>
        /// Notify that an error has occurred while attempting to delete a record.
        /// </summary>
        protected virtual void OnDeleteError(Exception exception)
        {
            TempData.Error(string.Format("Unable to delete record. Error: {0}", exception.Message));
        }

        /// <summary>
        /// Notify that a delete has been requested.
        /// </summary>
        protected virtual void OnDeleteRequested(object id)
        {            
        }

        /// <summary>
        /// Notify that a delete operation has been successful.
        /// </summary>
        protected virtual void OnDeleteSuccessful(object id)
        {
            TempData.Success("Successfully deleted record.");
        }

        protected virtual void OnEditError(Exception exception)
        {
        }

        protected virtual void OnEditModelStateInvalid(TModel model)
        {
            TempData.Warning("There are problems with the form.");
        }

        protected virtual void OnEditRequested(object id)
        {            
        }

        protected virtual void OnEditSuccessful(TModel model)
        {
            TempData.Success("Successfully updated record.");
        }

        protected virtual void OnIndexRequested()
        {
        }

        protected virtual void OnListRequested(int? page)
        {
            pageIndex = (page == null) ? 0 : page.Value - 1;
        }

        protected virtual void OnNewError(Exception exception)
        {
            TempData.Error("Unable to add record.");
        }

        protected virtual void OnNewModelStateInvalid(TModel model)
        {
            TempData.Warning("There are problems with the form.");
        }

        protected virtual void OnNewRequested()
        {
        }

        protected virtual void OnNewSuccessful(TModel model)
        {
            TempData.Success("Successfully added record.");
        }

        protected virtual void OnShowRequested(object id)
        {            
        }

        protected virtual ActionResult ReturnDeleteError(object id)
        {
            return View("DeleteError");
        }

        protected virtual ActionResult ReturnDeleteErrorXhr(object id, Exception exception)
        {
            throw new HttpException("There was an error while attempting to delete the given record.", exception);
        }

        protected virtual ActionResult ReturnDeleteSuccessful(object id)
        {
            return View("DeleteSuccessful");
        }

        protected virtual ActionResult ReturnDeleteSuccessfulXhr(object id)
        {
            return new EmptyResult();
        }

        protected virtual ActionResult ReturnEdit(TModel model)
        {
            return View("Edit", model);
        }

        protected virtual ActionResult ReturnEditSuccessful(object id, TModel model)
        {
            return (RedirectToIndex) ? RedirectToAction("Index") : RedirectToAction("Show", new { id });
        }

        protected virtual ActionResult ReturnEditSuccessfulXhr(object id, TModel model)
        {
            return (RedirectToIndex) ? Index() : Show(id);
        }

        protected virtual ActionResult ReturnEditXhr(TModel model)
        {
            return View("EditControl", model);
        }

        protected virtual ActionResult ReturnList(IEnumerable<TModel> collection)
        {
            return View("List", collection);
        }

        protected virtual ActionResult ReturnListXhr(IEnumerable<TModel> collection)
        {
            return PartialView("ListControl", collection);
        }

        protected virtual ActionResult ReturnNew(TModel model)
        {
            return View("New", model);
        }

        protected virtual ActionResult ReturnNewSuccessful(object id, TModel model)
        {
            return (RedirectToIndex) ? RedirectToAction("Index") : RedirectToAction("Show", new { id });
        }

        protected virtual ActionResult ReturnNewSuccessfulXhr(object id, TModel model)
        {
            return (RedirectToIndex) ? Index() : Show(id);
        }

        protected virtual ActionResult ReturnNewXhr(TModel model)
        {
            return PartialView("NewControl", model);
        }

        protected virtual ActionResult ReturnShow(TModel model)
        {
            return View("Show", model);
        }

        protected virtual ActionResult ReturnShowXhr(TModel model)
        {
            return PartialView("ShowControl", model);
        }

        /// <summary>
        /// Update the given resource. Provided as an extension point, since you may
        /// want to write your own left-right binding code, instead of relying on the
        /// default model binder.
        /// </summary>
        /// <param name="original">Original model, newly fetched from the data store.</param>
        /// <param name="form">Updated model, just came in from the form.</param>
        protected virtual void UpdateResource(TModel original, TModel form)
        {
            UpdateModel(original);
        }

        #endregion
    }
}
